package org.xx.armory.spring5.mvc;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.client.HttpClientErrorException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Locale;

import static java.lang.Integer.toHexString;
import static java.lang.String.join;
import static java.lang.System.identityHashCode;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static org.springframework.core.annotation.AnnotationUtils.findAnnotation;
import static org.xx.armory.commons.DebugUtils.dumpTraceBack;
import static org.xx.armory.commons.TextUtils.format;
import static org.xx.armory.spring5.mvc.WebUtils.getFullPath;

@ControllerAdvice
public class GlobalErrorHandler {
    public static final String RUNTIME_ERROR_MARKER = "RUNTIME_ERROR";
    private static final Marker RUNTIME_ERROR_MARKER_ = MarkerFactory.getMarker(RUNTIME_ERROR_MARKER);
    private static final String ERROR_FORMAT = "{} \"{}\" -> {}\n{}";

    private final Logger logger = LoggerFactory.getLogger("org.xx.armory.spring5.error");

    @ExceptionHandler
    public void handle(
            HttpServletRequest request,
            HttpServletResponse response,
            Locale local,
            Exception error
    )
            throws IOException {
        if (error == null) {
            return;
        }

        final var ex = simplifyException(error);
        if (ex instanceof Error) {
            throw (Error) ex;
        } else if (ex == null) {
            return;
        }

        final var fullPath = getFullPath(request);

        if (ex instanceof ServletRequestBindingException) {
            // 绑定参数出错，应当返回BAD_REQUEST响应，不需要记录。
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
        } else if (ex instanceof HttpRequestMethodNotSupportedException) {
            // Controller不支持HTTP Method，应当返回BAD_REQUEST响应，不需要记录。
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
        } else if (ex instanceof HttpClientErrorException) {
            // 客户端错误。
            final var hcEx = (HttpClientErrorException) ex;
            response.sendError(hcEx.getRawStatusCode(), ex.getMessage());
        } else {
            // 通过 HttpServletResponse.sendError发送的errorMessage会被ErrorAttributes截获，并通过message字段传递给客户端。
            final var requestId = "$" + toHexString(identityHashCode(request)) + "$";
            final var errorMessage = requestId + " " + ex.getMessage();
            final var tb = join("\n", dumpTraceBack(ex));

            // 其它错误，首先检查是否指定了 `ResponseStatus` 注解。
            final var a = findAnnotation(ex.getClass(), ResponseStatus.class);
            if (a != null) {
                // 根据注解返回对应的响应。
                if (a.code().is4xxClientError()) {
                    // 客户端错误，应当返回对应的客户端错误代码，记录客户端错误代码。
                    logger.warn(format(ERROR_FORMAT, request.getMethod(), fullPath, a.code().value(), tb));
                    response.sendError(a.code().value(), errorMessage);
                } else if (a.code().is5xxServerError()) {
                    // 服务器内部错误，应当返回INTERNAL_SERVER_ERROR，记录请求ID和调用栈。
                    logger.error(format(ERROR_FORMAT, request.getMethod(), fullPath, "Runtime error (" + requestId + ")", tb));
                    response.sendError(a.code().value(), errorMessage);
                }
            } else {
                // 未指定注解的错误，也作为服务器内部错误，应当返回INTERNAL_SERVER_ERROR，记录请求ID和调用栈。
                logger.error(RUNTIME_ERROR_MARKER_,
                             format(ERROR_FORMAT, request.getMethod(), fullPath, "Runtime error (" + requestId + ")", tb));
                response.sendError(SC_INTERNAL_SERVER_ERROR, errorMessage);
            }
        }
    }

    /**
     * 简化异常对象。
     *
     * <p>如果遇到 {@link BusinessLogicException} 则中断查找。</p>
     *
     * @param t
     *         原始异常对象。
     * @return {@code ex}对象的根异常对象。
     * @see Throwable#getCause()
     */
    Throwable simplifyException(
            Throwable t
    ) {
        if (t == null) {
            return null;
        }

        var r = t;
        Throwable p;
        while ((p = r.getCause()) != null) {
            if (p instanceof Error) {
                return p;
            }

            if (p instanceof BusinessLogicException) {
                return p;
            }

            r = p;
        }

        return r;
    }
}
